﻿using SharpDepend.Datatypes;
using System;
using System.Collections.Generic;
using System.Text;

namespace SharpDepend.Classes
{
    public interface ITouchControl
    {
        /// <summary>
        /// Get current touch state.
        /// <para>Including mouse pointer as a touch on Windows.</para>
        /// </summary>
        /// <returns>Collection of current touches.</returns>
        TouchCollection GetState();
    }

    public delegate bool EventHandlerMouseDownAction(ref TouchLocation location, int id, int touchCount);
    public delegate void EventHandlerMouseAction(ref TouchLocation location, int id, int touchCount);
    public delegate void EventHandlerMouseWheelAction(ref TouchLocation location);

    public interface ITouchListener
    {
        event EventHandlerMouseDownAction PointDown;
        event EventHandlerMouseAction PointMove;
        event EventHandlerMouseAction PointUp;
        event EventHandlerMouseWheelAction PointWheel;
        event EventHandlerMouseAction PointHover;

        bool OnMouseDown(ref TouchLocation location, int id, int touchCount);
        void OnMouseMove(ref TouchLocation location, int id, int touchCount);
        void OnMouseUp(ref TouchLocation location, int id, int touchCount);
        void OnMouseWheel(ref TouchLocation location);
        void OnMouseHover(ref TouchLocation mousestate, int id, int touchCount);
    }

    public class TouchManager
    {
        private class Mapper : IEquatable<ITouchListener>, IComparable<ITouchListener>
        {
            private Dictionary<int, int> mTouchIds;

            public readonly ITouchListener Listener;

            public Mapper(ITouchListener listener)
            {
                if (listener == null)
                {
                    throw new ArgumentNullException(nameof(listener));
                }

                mTouchIds = new Dictionary<int, int>();
                Listener = listener;
            }

            public static bool operator ==(Mapper left, ITouchListener right)
            {
                return left.Listener == right;
            }

            public static bool operator !=(Mapper left, ITouchListener right)
            {
                return left.Listener != right;
            }

            public bool Equals(ITouchListener other)
            {
                return Listener == other;
            }

            public int CompareTo(ITouchListener other)
            {
                return Listener.GetHashCode() - other.GetHashCode();
            }

            private int GetUniqueTouchId()
            {
                // Start with id 0.
                int uniqueId = 0;

                // Increase the id until we got a unique id.
                while (mTouchIds.ContainsValue(uniqueId))
                {
                    ++uniqueId;
                }

                // Return the unique id.
                return uniqueId;
            }

            public void Update(TouchCollection touches)
            {
                int touchCount = touches.Count;
                for (int i = 0; i < touchCount; i++)
                {
                    TouchLocation mousestate = touches[i];

                    int realId = mousestate.ID;

                    if (mousestate.State == TouchLocationState.Pressed)
                    {
                        int id = int.MinValue; // Invalid value.

                        // If the id already exist. Release state has not been called.
                        // Anyway, fix it just to use the old id.
                        if (mTouchIds.ContainsKey(realId))
                        {
                            id = mTouchIds[realId];
                        }
                        else
                        {
                            // Get unique id.
                            id = GetUniqueTouchId();

                            // If id deosen't exist, add it.
                            //if (!mTouchIds.ContainsKey(realId))
                            //{
                                mTouchIds.Add(realId, id);
                            //}
                        }

                        // Call mouse event.
                        Listener.OnMouseDown(ref mousestate, id, touchCount);
                    }
                    else if (mousestate.State == TouchLocationState.Moved)
                    {
                        // If the touch has not exist. Then ignore this moving (It has no meaning).
                        if (mTouchIds.ContainsKey(realId))
                        {
                            // Get id by real id.
                            int id = mTouchIds[realId];

                            // Call mouse event.
                            Listener.OnMouseMove(ref mousestate, id, touchCount);
                        }
                    }
                    else if (mousestate.State == TouchLocationState.Released /*|| mousestate.State == TouchLocationState.Invalid*/)
                    {
                        // If the touch has not exist. Then ignore this release (It has no meaning).
                        if (mTouchIds.ContainsKey(realId))
                        {
                            // Get id by real id.
                            int id = mTouchIds[realId];

                            // Remove the id.
                            mTouchIds.Remove(realId);

                            // Call mouse event.
                            Listener.OnMouseUp(ref mousestate, id, touchCount);
                        }
                    }
                    else if (mousestate.State == TouchLocationState.Hovering)
                    {
                        // Call mouse event.
                        Listener.OnMouseHover(ref mousestate, mousestate.ID, touchCount);
                    }
                    else
                    {
                        // Check if the id was in use just before. Can be it if this goes invalid.
                        if (mTouchIds.ContainsKey(realId))
                        {
                            // Get id by real id.
                            int id = mTouchIds[realId];

                            // Remove the id (this event is invalid, or can be a mouse wheel).
                            mTouchIds.Remove(realId);
                        }

                        if (mousestate.IsMouse)
                        {
                            if (mousestate.MouseWheel != 0.0)
                            {
                                Listener.OnMouseWheel(ref mousestate);
                            }
                        }
                    }
                }
            }
        }

        private Dictionary<ITouchControl, List<Mapper>> mTouchListeners;
        private StringBuilder mDebugReportSb;
        private IGuiTimer mTimer;
        private HashSet<ITouchListener> mDisabledListeners = new HashSet<ITouchListener>();

        public static TouchManager Instance { get; private set; }

        public bool DebugMode { get; set; }

        public int TouchCount { get; private set; }
        
        public TouchManager(IGUITimerManager guiTimerManager)
        {
            mTouchListeners = new Dictionary<ITouchControl, List<Mapper>>();

            mTimer = guiTimerManager.CreateTimer();
            mTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
            mTimer.Tick += application_OnGUIUpdate;
            mTimer.Start();

            mDebugReportSb = new StringBuilder();

            Instance = this;
        }
        
        void application_OnGUIUpdate()
        {
            bool debugMode = DebugMode;
            if (debugMode)
            {
                mDebugReportSb.Clear();
            }

            // Loop all touch controllers.
            foreach (var item in mTouchListeners)
            {
                // Get touch state from the touch controller.
                TouchCollection touches = item.Key.GetState();

                TouchCount = touches.Count;

                if (touches.Count > 0)
                {
                    // For debugging.
                    if (debugMode)
                    {
                        for (int i = 0; i < touches.Count; i++)
                        {
                            TouchLocation touch = touches[i];
                            mDebugReportSb.AppendLine("ID: " + touch.ID);
                            mDebugReportSb.AppendLine("State: " + touch.State.ToString());
                            mDebugReportSb.AppendLine("X: " + touch.X + ", Y: " + touch.Y);
                        }
                    }

                    // Loop all listener to update them with the touche states.
                    foreach (var list in item.Value)
                    {
                        // Check so it is not disabled.
                        if (!mDisabledListeners.Contains(list.Listener))
                        {
                            list.Update(touches);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Enable or disable listener.
        /// </summary>
        /// <param name="listener"></param>
        /// <param name="enable"></param>
        public void EnableListener(ITouchListener listener, bool enable)
        {
            if (enable)
            {
                mDisabledListeners.Remove(listener);
            }
            else
            {
                mDisabledListeners.Add(listener);
            }
        }

        /// <summary>
        /// Register toucheable components.
        /// </summary>
        /// <param name="listener">Component to listen to touches.</param>
        /// <param name="controller">The touch controller.</param>
        public void RegisterTouchListener(ITouchListener listener, ITouchControl controller)
        {
            if (listener == null)
            {
                throw new ArgumentNullException(nameof(listener));
            }
            if (controller == null)
            {
                throw new ArgumentNullException(nameof(controller));
            }

            foreach (var item in mTouchListeners)
            {
                foreach (var mapper in item.Value)
                {
                    if (mapper == listener)
                    {
                        throw new Exception("Listener already exist in a controller. It must be removed first.");
                    }
                }
            }

            if (!mTouchListeners.ContainsKey(controller))
            {
                mTouchListeners.Add(controller, new List<Mapper>());
            }

            mTouchListeners[controller].Add(new Mapper(listener));
        }

        /// <summary>
        /// Unregister touchable component.
        /// </summary>
        /// <param name="listener">Component to listen to touches.</param>
        /// <param name="controller">The touch controller.</param>
        public void UnregisterTouchListener(ITouchListener listener, ITouchControl controller)
        {
            if (listener == null)
            {
                throw new ArgumentNullException(nameof(listener));
            }
            if (controller == null)
            {
                throw new ArgumentNullException(nameof(controller));
            }

            if (!mTouchListeners.ContainsKey(controller))
            {
                throw new ArgumentOutOfRangeException("Controller not found.");
            }

            var listOfListeners = mTouchListeners[controller];
            bool notFound = true;
            foreach (var item in listOfListeners)
            {
                if (item == listener)
                {
                    notFound = false;
                    listOfListeners.Remove(item);
                    break;
                }
            }
            if (notFound)
            {
                throw new ArgumentOutOfRangeException("Listener not found.");
            }
        }

        /// <summary>
        /// For debugging. Report the current touch state.
        /// </summary>
        /// <returns>Report.</returns>
        public string ReportState()
        {
            if (DebugMode)
            {
                return mDebugReportSb.ToString();
            }

            return "DebugMode is off.";
        }
    }
}
